---
title: 安全性、活跃性以及性能问题
description: 并发编程中需要注意的问题主要有三方面，分别是：安全性问题、活跃性问题和性能问题
---



## 安全性问题
线程安全本质上就是正确性，而正确性的含义就是程序按照我们期望的执行。

并发BUG的三个主要源头是 原子性、可见性和有序性问题。也就是说，要想写出写出线程安全的程序，就需要避免出现原子性、可见性和有序性问题。

但并不是所有的代码都需要认真分析一遍这三个问题？只有一种情况是需要的：**存在共享数据并且该数据会发生变化（多个线程会同时读写同一数据）。**

所以如果能够做到不共享数据或者数据状态不发生变化，就能够保证线程的安全性。

但现实中，**必须共享会发生变化的数据**，这样的应用场景有很多。



当多个线程同时访问同一数据，并且至少有一个线程会写这个数据时，如果不采取措施，那么就会导致并发 BUG。这种情况有一个专业术语叫做**数据竞争**(Data Race)

```java title='数据竞争示例'
public class Test{
    private long count = 0;
    void add10k() {
        int idx = 0;
        while(idx ++< 10000){
            count += 1;
        }
    }
}
```

那是否在访问数据的地方，加个锁保护一下就能解决所有并发问题了呢？

对于上述示例代码，稍作修改，增加两个被 synchronized 修饰的 get() 和 set() 方法，add10K() 方法里通过 get() 和 set() 方法来访问 value 变量。

```java
public class Test{
    private long count = 0;
    synchronized long get(){
        return count;
    }
    synchronized void set(long v){
        count = v;
    }
    void add10K(){
        int idx = 0;
        while(idx++ < 10000){
            set(get()+1)
        }
    }
} 
```

对于修改后的代码，所有访问共享数据 value 的地方，此时是不存在数据竞争的。但是修改后的 add10K方法并不是线程安全的。

这种问题，有个官方的称呼，叫做竞态条件，指的是** 程序的执行结果依赖线程执行的顺序**。

例如上面的例子，如果两个线程完全同时执行，那么结果是 1；如果两个线程是前后执行，那么结果就是 2；

面对数据竞争和竞态条件问题，都可以采用互斥的技术方案解决。

## 活跃性问题

活跃性问题，指的是某个操作无法执行下去，死锁就是一个典型的活跃性问题，**除了死锁外，还有两种情况，分别是活锁和饥饿**。

发生死锁后线程会相互等待，而且会一直等待下去，在技术上表现形式是线程永久地阻塞了。

但有时线程虽然没有发生阻塞，但仍然会存在执行不下去的情况，这就是所谓的**活锁**

解决活锁问题的方案很简单，谦让时，尝试等待一个随机的时间就可以了。Raft 这样的分布式一致性算法中就用到了它。

**饥饿指的是线程因无法访问所需资源而无法执行下去的情况**。如果线程优先级不均，在  CPU 繁忙的时候，优先级低的线程得到执行的机会很小，就可能发生线程饥饿；持有锁的线程，如果执行时间过长，也可能导致饥饿问题。

解决死锁问题有三个方案：一是保证资源充足，而是公平地分配资源；三是避免持有锁的线程长时间执行。方案二的适用场景相对多一些。

公平分配资源主要是使用公平锁。是一种先来后到的方案，线程的等待是有顺序的，排在等待队列前面的线程会优先获得资源。

## 性能问题

锁的过度使用可能会导致串行化的范围过大，这样就不能发挥多线程的优势了。之所以搞并发程序，为的就是提升性能。

1. 最好使用无锁的算法和数据结构，如线程本地存储、写入时复制、乐观锁等；JUC 中的原子类也是一种无锁的数据结构；Disruptor 则是一个无所的内对队列，性能都非常好。
2. 减少持有锁的时间。互斥锁本质上是将并行的程序串行化，所以要增加并行度，一定要减少持有锁的时间。例如使用细粒度锁，比如 JUC 里的 ConcurrentHashMap ，它使用了分段锁的技术；还可以使用读写锁，也就是读是无锁的，只有写的时候才会互斥。

